#!/usr/bin/python
# Samba script parser

import sys
from backend import mainPath
sys.path.append('.')
from backend.tune import Tune, INSTRUMENTS, ParseError, word_split

TEMPO = 120.
TUNES = dict([])
class Script:
		"Samba script"
		def __init__(self, filename):
				text = file(filename, 'r').read().splitlines()
				text = [line.strip() for line in text]
				text = [(num+1, line) for (num, line) in enumerate(text) if line != '' and line[0] != '#']
				# init
				time = 0.
				tempo = TEMPO
				tbar = 60./tempo
				loudness_global = 1
				loudness = dict([])
				forces = dict([])
				status = dict([]) # on (default), off or keep
				tune = None
				next_snare_sh = 1
				self.sequence = []
				# parse
				for line_number, line in text:
						pline = word_split(line)
						keyword = pline[0].lower()
						if keyword == 'tune':
								tune = pline[1] # N.B. no spaces and retains case!
								if tune not in TUNES:
										try:
												TUNES[tune] = Tune(mainPath+tune + '.tune')
										except IOError:
												raise ParseError(line_number, 'missing tune file')
										except ParseError as pe:
												raise ParseError(pe.line_number, tune + ': ' + pe.error)
						elif keyword == 'tempo':
								try:
										tempo = float(pline[1])
										tbar = 60./tempo
								except ValueError:
										raise ParseError(line_number, 'malformed tempo')
								except IndexError:
										raise ParseError(line_number, 'missing tempo')
						elif keyword == 'loudness':
								if len(pline) == 2:
										try:
												loudness_global = float(pline[1])
										except ValueError:
												raise ParseError(line_number, 'malformed loudness')
								else:
										try:
												loudness[pline[1]] = float(pline[2])
										except ValueError:
												raise ParseError(line_number, 'malformed loudness')
										except IndexError:
												raise ParseError(line_number, 'missing loudness')
						elif keyword == 'forces':
								try:
										forces[pline[1]] = int(pline[2])
								except ValueError:
										raise ParseError(line_number, 'malformed forces')
								except IndexError:
										raise ParseError(line_number, 'missing forces')
						elif keyword in ['on', 'off', 'keep']:
								if len(pline) == 1:
										raise ParseError(line_number, '%s without instruments' % keyword)
								for inst0 in ' '.join(pline[1:]).split('+'):
										inst = inst0.strip().lower()
										for minst in INSTRUMENTS[inst]:
												status[minst] = keyword
						elif keyword == 'silence':
								entry, counts = self.parse(line_number, line)
								time += tbar * counts
						else:
								if tune is None:
										raise ParseError(line_number, "no tune")
								entry, counts = self.parse(line_number, line)
								if entry not in TUNES[tune].entries:
										raise ParseError(line_number, "unknown entry %s in tune %s" % (entry, tune))
								entry = TUNES[tune].entries[entry]
								# prepare entry
								music = dict([(inst, []) for inst in entry.music])
								# on instruments
								dtime = 0
								for repeat in range(counts):
										for inst in entry.music:
												if inst not in status:
														status[inst] = 'on'
												if status[inst] == 'on':
														music[inst] += [(dtime + t, sample, mag) for (t, sample, mag) in entry.music[inst]]
										dtime += entry.length * entry.bar_length
								# groove (keep) instruments
								# N.B. if groove is cut in the middle, it will restart
								groove = TUNES[tune].entries['Groove']
								repeats = int(float(counts) * entry.length / groove.length + 0.999)
								dtime = 0
								for repeat in range(repeats):
										for inst in entry.music:
												if status[inst] == 'keep':
														music[inst] += [(dtime + t, sample, mag) for (t, sample, mag) in groove.music[inst]]
										dtime += groove.length * groove.bar_length
								for inst in entry.music:
										if status[inst] == 'keep':
												music[inst] = [(t, sample, mag) for (t, sample, mag) in music[inst] if t < entry.length * entry.bar_length * counts]
								# realize entry
								for inst in music:
										# shaker?
										if inst == 'shaker' and tempo != TEMPO:
												continue
										# determine loudness
										mag = loudness_global
										if inst in loudness:
												mag *= loudness[inst]
										if inst in forces:
												mag *= forces[inst]
										for t, sample, loud in music[inst]:
												if sample == '':
														sample_name = inst
												else:
														sample_name = inst + '_' + sample
												# snare?
												if inst == 'snare':
														if sample == 'X':
																next_snare_sh = 1
														else:
																sample_name += '_%d' % next_snare_sh
														next_snare_sh = 3 - next_snare_sh
												sample_name += '.wav'
												self.sequence.append((time + t * (tbar/entry.bar_length), sample_name, loud * mag))
								# advance time
								time += entry.tlength * tbar * counts
								# reset grooved instruments if need be
								if entry.type == 'Break':
										for inst in status:
												if status[inst] == 'keep':
														status[inst] = 'on'
		def parse(self, line_number, line):
				"parse line into entry name and (optionally) repeat count"
				pline = word_split(line)
				if pline[-1].startswith('(') and pline[-1].endswith(')'):
						try:
								counts = int(pline[-1][1:-1])
						except ValueError:
								raise ParseError(line_number, "malformed number of repeats")
						pline = pline[:-1]
				else:
						counts = 1
				return ' '.join(pline), counts

def doWork():
		from backend.mixer import Mixer
		if len(sys.argv) != 3:
				print "usage: %s script output" % sys.argv[0]
		else:
				script = sys.argv[1]
				output = sys.argv[2]
				try:
						S = Script(script)
						Mixer(S.sequence).mix(output)
				except ParseError as pe:
						print "error in line %d: %s" % (pe.line_number, pe.error)

if __name__ == "__main__":
	doWork()